package main;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;

import javax.swing.*;
import serial.SerialException;
import serial.SerialInterface;

import gui.*;

@SuppressWarnings("serial")
public class SerialFrame extends JFrame implements UndoableTextAreaListener {
	private SerialInterface serialIface;
	private SendPanel sendPanel;
	private HistoryTextField lineInput;
	private JTabbedPane programTabs;
	private ArrayList<UndoableTextArea> programs;
	private ArrayList<File> programFiles;

	private Quitter q;
	private JMenuItem newFile, open, save, saveAs, close, exit;
	private JMenuItem setPortName, showHideOutput, showHideMemory;
	private JCheckBoxMenuItem checkCode;
	private JRadioButtonMenuItem pickLibrary, pickSonar, pickNone;
	private ButtonGroup libraryGroup;
	private JMenuItem undo, redo;
	private JMenuItem info;
	private JFileChooser luaChooser;
	
	private OutputFrame outputWindow;
	private MemoryFrame memoryWindow;

	private final static Font COURIER = new Font("Courier", Font.PLAIN, 12);
	public static final String LOG_PATH = ".pbllog";
	public static final String DEFAULT_TITLE = "New";
	// public static final String LOG_PATH = "C:\\cygwin\\home\\burch\\log.txt";
	private final static String PORT_PATH = ".portNames";
	private final static String PROGRAM_TITLE = "pbLua Controller";
	private final static double VERSION = 0.22;
	
	public SerialFrame() throws SerialException, IOException {
		outputWindow = new OutputFrame();
		serialIface = new SerialInterface(outputWindow);
		memoryWindow = new MemoryFrame(serialIface);
		luaChooser = new LuaChooser();
		setupFrame();
		setupControls();
		setupMenus();
		setupPastPorts();
	}
	
	private UndoableTextArea program() {
		return programs.get(progIndex());
	}
	
	private int progIndex() {
		return programTabs.getSelectedIndex();
	}
	
	private void resetCurrentTitle() {
		String title = DEFAULT_TITLE;
		if (programFiles.get(progIndex()) != null) {
			title = programFiles.get(progIndex()).getName();
		}
		if (program().needsSaving()) {
			title = "*" + title;
		}
		programTabs.setTitleAt(progIndex(), title);
	}

	@Override
	public void statusChanged(UndoableTextArea uta) {
		resetCurrentTitle();
	}

	private void setupFrame() {
		setSize(800, 400);
		setTitle(PROGRAM_TITLE);
		setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
		q = new Quitter();
		addWindowListener(q);
	}

	private void setupControls() {
		sendPanel = new SendPanel(this, serialIface);
		
		lineInput = new HistoryTextField(30);
		lineInput.setFont(COURIER);
		
		JPanel lineInputPanel = new JPanel();
		lineInputPanel.setLayout(new BorderLayout());
		lineInputPanel.add(lineInput, BorderLayout.CENTER);
		lineInputPanel.setBorder(BorderFactory.createTitledBorder("Command Line"));

		lineInput.addActionListener(new WriterSender());

		JPanel areaPanel = new JPanel();
		areaPanel.setLayout(new GridLayout(1, 1));
		
		programTabs = new JTabbedPane();
		programs = new ArrayList<UndoableTextArea>();
		programFiles = new ArrayList<File>();
		addNewTab();
		programTabs.setBorder(BorderFactory.createTitledBorder("Programs"));
		areaPanel.add(programTabs);
		
		getContentPane().setLayout(new BorderLayout());
		getContentPane().add(lineInputPanel, BorderLayout.NORTH);
		getContentPane().add(areaPanel, BorderLayout.CENTER);
		getContentPane().add(sendPanel, BorderLayout.SOUTH);
	}
	
	private int numPrograms() {
		return programs.size();
	}
	
	private int maxProgIndex() {
		return numPrograms() - 1;
	}
	
	private void addNewTab() {
		UndoableTextArea uta = new UndoableTextArea();
		uta.addUndoableTextAreaListener(this);
		uta.addUndoableTextAreaListener(sendPanel);
		programs.add(uta);
		programTabs.add(DEFAULT_TITLE, createTextPanel(programs.get(maxProgIndex())));		
		programTabs.setSelectedIndex(maxProgIndex());
		programFiles.add(null);
	}

	private JPanel createTextPanel(JTextArea area) {
		area.setFont(COURIER);
		JPanel areaPanel = new JPanel();
		areaPanel.setLayout(new BorderLayout());
		areaPanel.add(new LineNumberedPane(area), BorderLayout.CENTER);
		return areaPanel;
	}

	private void setupMenus() {
		JMenuBar bar = new JMenuBar();
		JMenu fileMenu = new JMenu("File");
		bar.add(fileMenu);

		newFile = new JMenuItem("New");
		newFile.addActionListener(new Newer());
		fileMenu.add(newFile);
		//newFile.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, InputEvent.CTRL_DOWN_MASK));

		Opener opener = new Opener();

		open = new JMenuItem("Open");
		open.addActionListener(opener);
		fileMenu.add(open);
		open.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_DOWN_MASK));
		
		close = new JMenuItem("Close");
		close.addActionListener(new Closer());
		fileMenu.add(close);
		//close.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, InputEvent.CTRL_DOWN_MASK));
		
		Saver saver = new Saver();

		save = new JMenuItem("Save");
		save.addActionListener(saver);
		fileMenu.add(save);
		save.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent.CTRL_DOWN_MASK));

		saveAs = new JMenuItem("Save As...");
		saveAs.addActionListener(saver);
		fileMenu.add(saveAs);
		
		fileMenu.addSeparator();
		
		exit = new JMenuItem("Exit");
		exit.addActionListener(q);
		fileMenu.add(exit);
		
		JMenu editMenu = new JMenu("Edit");
		bar.add(editMenu);
		
		undo = new JMenuItem("Undo");
		undo.addActionListener(new Undoer());
		editMenu.add(undo);
		undo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, InputEvent.CTRL_DOWN_MASK));
		
		redo = new JMenuItem("Redo");
		redo.addActionListener(new Redoer());
		editMenu.add(redo);
		redo.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, InputEvent.CTRL_DOWN_MASK));
		
		JMenu robotMenu = new JMenu("NXT");
		bar.add(robotMenu);
		
		checkCode = new JCheckBoxMenuItem("Syntax Checking");
		checkCode.addActionListener(new CodeCheckerEnabler());
		checkCode.setSelected(true);
		robotMenu.add(checkCode);

		setPortName = new JMenuItem("Configure Serial Port");
		setPortName.addActionListener(new SerialPortSetter());
		robotMenu.add(setPortName);
		
		showHideOutput = new JMenuItem("Show Output");
		showHideOutput.addActionListener(new WindowShower(outputWindow));
		robotMenu.add(showHideOutput);
		
		showHideMemory = new JMenuItem("Memory Usage");
		showHideMemory.addActionListener(new WindowShower(memoryWindow));
		robotMenu.add(showHideMemory);
		
		JMenu libraryMenu = new JMenu("Library");
		pickLibrary = new JRadioButtonMenuItem("Standard library");
		pickSonar = new JRadioButtonMenuItem("Sonar library only");
		pickNone = new JRadioButtonMenuItem("No library");
		LibrarySelector handler = new LibrarySelector();
		pickLibrary.addActionListener(handler);
		pickSonar.addActionListener(handler);
		pickNone.addActionListener(handler);
		libraryGroup = new ButtonGroup();
		libraryGroup.add(pickLibrary);
		libraryGroup.add(pickSonar);
		libraryGroup.add(pickNone);
		libraryMenu.add(pickLibrary);
		libraryMenu.add(pickSonar);
		libraryMenu.add(pickNone);
		pickLibrary.setSelected(true);
		bar.add(libraryMenu);
		
		JMenu helpMenu = new JMenu("Help");
		bar.add(helpMenu);
		
		info = new JMenuItem("Version");
		info.addActionListener(new Abouter());
		helpMenu.add(info);	

		setJMenuBar(bar);
	}
	
	private class Newer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			addNewTab();
		}
	}
	
	private class Undoer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			program().undo();
		}
	}
	
	private class Redoer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			program().redo();
		}
	}
	
	private class Closer implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			save(false, true);
			int target = progIndex();
			programTabs.remove(target);
			programs.remove(target);
			programFiles.remove(target);
		}
	}
	
	private class WindowShower implements ActionListener {
		JFrame window;
		public WindowShower(JFrame window) {
			this.window = window;
		}
		public void actionPerformed(ActionEvent e) {
			window.setVisible(true);
			if (window == memoryWindow) {
				memoryWindow.requestInfo();
			}
		}
	}
	
	private class SerialPortSetter implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			setSerialPortName();
		}
	}

	private void setSerialPortName() {
		String value = JOptionPane.showInputDialog("Enter serial port name:");
		if (value != null && !value.equals("")) {
			serialIface.suggestBestPort(value);
		}
	}

	private class WriterSender implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendSingleCommand(lineInput.getText());
		}
	}
	
	private void sendSingleCommand(String cmd) {
		boolean sent = sendPanel.send(cmd + "\n", "command", false, false);
		if (sent) {
			lineInput.archiveAndClear();
		}
	}
	
	public void sendProgram(boolean unplug) {
		sendPanel.send(program().getText(), "program", unplug, true);
	}

	private class Quitter extends WindowAdapter implements ActionListener {
		public void windowClosing(WindowEvent arg0) {
			quit();
		}

		public void actionPerformed(ActionEvent e) {
			quit();
		}
	}

	private void quit() {
		saveAll();
		savePortNames();
		System.exit(0);
	}
	
	private class Abouter implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			JOptionPane.showMessageDialog(null, PROGRAM_TITLE + " " + VERSION + " (Maintainer: Gabriel Ferrer; ferrer@hendrix.edu)");
		}
	}

	private class Opener implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			int option = luaChooser.showOpenDialog(SerialFrame.this);
			if (option == JFileChooser.APPROVE_OPTION) {
				File choice = luaChooser.getSelectedFile();
				addNewTab();
				try {
					setFileForTab(choice);
					program().open(choice);
				} catch (FileNotFoundException e1) {
					JOptionPane.showMessageDialog(null, "Problem opening " + choice.getName());
				}
			}
		}
	}
	
	private class LibrarySelector implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			if (pickLibrary.isSelected()) {
				serialIface.enableFullLibrary();
				sendPanel.setUnplugOption(true);
			} else if (pickSonar.isSelected()) {
				serialIface.enableSonarLibrary();
				sendPanel.setUnplugOption(false);
			} else if (pickNone.isSelected()) {
				serialIface.disableLibrary();
				sendPanel.setUnplugOption(false);
			}
		}
	}
	
	private class CodeCheckerEnabler implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			sendPanel.setChecking(checkCode.isSelected());
		}
	}
	
	private void setFileForTab(File choice) {
		programFiles.set(progIndex(), choice);
		resetCurrentTitle();
	}
	
	private class Saver implements ActionListener {
		public void actionPerformed(ActionEvent e) {
			save(e.getSource() == saveAs, false);
		}
	}
	
	private void saveAll() {
		int current = programTabs.getSelectedIndex();
		save(false, true);
		for (int i = 0; i < programTabs.getTabCount(); ++i) {
			if (i != current) {
				programTabs.setSelectedIndex(i);
				save(false, true);
			}
		}
		programTabs.setSelectedIndex(current);
	}

	private void save(boolean savingAs, boolean askFirst) {
		File choice = programFiles.get(progIndex());
		boolean saving = savingAs || program().needsSaving();
		if (saving && askFirst) {
			String title = programTabs.getTitleAt(progIndex());
			int result = JOptionPane.showConfirmDialog(null, "Save changes to \"" + title + "\"?", "Save changes?", JOptionPane.YES_NO_OPTION);
			saving = result == JOptionPane.YES_OPTION;
		}
		
		if (!saving) {return;}
		
		try {
			if (savingAs || choice == null) {
				// This solution did not quite work.  Hence, I resorted to the hack that follows.
				//luaChooser.setSelectedFile(choice == null ? luaChooser.getCurrentDirectory() : choice);
				if (choice == null) {
					choice = new File(luaChooser.getCurrentDirectory(), "New");
				}
				luaChooser.setSelectedFile(choice);
				
				int option = luaChooser.showSaveDialog(this);
				if (option == JFileChooser.APPROVE_OPTION) {
					choice = LuaChooser.withValidSuffix(luaChooser.getSelectedFile());
					program().save(choice);
					setFileForTab(choice);
				}
			} else {
				program().save(choice);
			}

		} catch (IOException ioe) {
			JOptionPane.showMessageDialog(null, choice == null ? "Cannot designate file for saving" : "Problem saving " + choice.getName());
		}
	}
	
	private String slurpFile(String name) {
		InputStream stream = getClass().getResourceAsStream(name);
		StringBuilder text = new StringBuilder();
		InputStreamReader in = null;
		try {
			in = new InputStreamReader(stream);
			char[] buf = new char[128];
			while (true) {
				int nchars = in.read(buf);
				if (nchars <= 0) break;
				text.append(buf, 0, nchars);
			}
			return text.toString();
		} catch (IOException e) {
			JOptionPane.showMessageDialog(null, "Problem loading " + name);
			return "";
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) { }
			}
		}
	}

	private void setupPastPorts() {
		try {
			serialIface.loadPossibleNamesFrom(PORT_PATH);
		} catch (FileNotFoundException e) {
			System.out.println("Port file does not exist for reading");
		}
	}

	private void savePortNames() {
		try {
			PrintWriter portOut = new PrintWriter(new FileWriter(new File(PORT_PATH)));
			for (String port : serialIface.getPossibleNames()) {
				portOut.println(port);
			}
			portOut.close();
		} catch (IOException fnfe) {
			System.out.println("Could not create port file");
		}
	}

	public static void main(String[] args) {
		try {
			new SerialFrame().setVisible(true);
		} catch (Exception e) {
			e.printStackTrace();
			System.exit(1);
		}
	}
}

